<?php 

namespace pmvc;

class Application {
	
	private $wrapperConfigs 		= Array();
	private $controllerConfigs 		= Array();
	private $viewHandlerConfigs 	= Array();
	private $dataSourceConfigs 		= Array();
	private $xml					= FALSE;
	private $fourOhFourController	= FALSE;
	
	private $requestWrapperMappings	= Array();
	private $controllerMappings		= Array();
	private $viewHandlerMappings	= Array();
	
    private $url				= FALSE;
    private $requestedUrl		= FALSE;
    private $contextUrl			= "";
    private $attributes			= Array();
    private $pendingRedirect	= FALSE;
    
    /**
     * Creates the application object.
     * @param $xml the XML configuration object
     */
    public function Application($xml, $url) {
    	$this->xml 	= $xml;
        $this->url = (strpos($url, '?')===FALSE)
            ? $url 
            : substr($url, 0, strpos($url, '?'));
        $parts = explode("/", getenv("SCRIPT_NAME"));
        $this->contextUrl = (count($parts)>0)
        	? str_replace("/".$parts[count($parts)-1], "", getenv("SCRIPT_NAME"))
        	: "";
       	pmvc_logdebug("context url is {0}", $this->contextUrl);
    }
    
    public function setAttribute($name, $value) {
    	$this->attributes[$name] = $value;
    }
    
    public function getAttribute($name) {
    	return isset($this->attributes[$name]) 
    		? $this->attributes[$name] : FALSE;
    }
    
    public function sendRedirect($url) {
    	$this->pendingRedirect = $url;
    }
    
    public function setFourOhFourController($controllerId) {
    	$this->fourOhFourController = $controllerId;
    }
    
	/**
	 * Adds a wrapper configuration to the Application.
	 * @param $config WrapperConfig the configuration to add
	 */
	public function addWrapperConfig(ObjectConfiguration $config) {
		$this->wrapperConfigs[$config->id] = $config;
	}
	
	/**
	 * Adds a controller configuration to the Application.
	 * @param $config ControllerConfig the configuration to add
	 */
	public function addControllerConfig(ObjectConfiguration $config) {
		$this->controllerConfigs[$config->id] = $config;
	}
	
	/**
	 * Adds a view handler configuration to the Application.
	 * @param $config ViewHandlerConfig the configuration to add
	 */
	public function addViewHandlerConfig(ObjectConfiguration $config) {
		$this->viewHandlerConfigs[$config->id] = $config;
	}
	
	/**
	 * Adds a data source configuration to the Application.
	 * @param $config DataSourceConfig the configuration to add
	 */
	public function addDataSourceConfig(ObjectConfiguration $config) {
		$this->dataSourceConfigs[$config->id] = $config;
	}
	
	/**
	 * Returns the DataSource with the given id.
	 * @param $id the DataSource's id
	 * @return DataSource
	 */
	public function getDataSource($id="default") {
		$cfg = $this->dataSourceConfigs[$id];
		if ($cfg->instance===FALSE) {
			$cfg->instance = pmvc_instantiate($cfg->class);
			$cfg->instance->configure($cfg->config);
			$cfg->instance->connect();
		}
		return $cfg->instance;
	}
	
	public function addRequestWrapperMapping($path, $toId, $basePath=NULL) {
		array_push($this->requestWrapperMappings, new MappingPair($path, $toId, $basePath));
	}
	
	public function addControllerMapping($path, $toId, $basePath=NULL) {
		array_push($this->controllerMappings, new MappingPair($path, $toId, $basePath));
	}
	
	public function addViewHandlerMapping($path, $toId, $basePath=NULL) {
		array_push($this->viewHandlerMappings, new MappingPair($path, $toId, $basePath));
	}

    /**
     * This function returns the requested url.
     * The requested url doesn't contain the
     * context path.
     * 
     * @return string the requested url
     *
     */
    public function getRequestedUrl() {
    	if ($this->requestedUrl==FALSE && $this->contextUrl!="") {
	    	$pos = strpos($this->url, $this->contextUrl);
	    	$this->requestedUrl = ($pos!==FALSE && $pos==0) 
	    		? substr($this->url, strlen($this->contextUrl)) : $this->url;
	    	pmvc_logdebug("requestedUrl parsed as {0}", $this->requestedUrl);
	    	
    	} else if ($this->requestedUrl==FALSE && $this->contextUrl=="") {
	    	$this->requestedUrl = $this->url;
	    	pmvc_logdebug("requestedUrl parsed as {0}", $this->url);
    	}
    	
    	return $this->requestedUrl;
    }

    /**
     * This function returns the context url.
     * 
     * @return string the context url
     *
     */
    public function getContextUrl() {
    	return $this->contextUrl;
    }

    /**
     * Checks to see if the given url matches the MappingPair
     * @param unknown_type $url the url
     * @param unknown_type $mappingPair the MappingPair
     */
    private function doesUrlMatchMappingPair($url, $mappingPair) {
    	pmvc_logdebug("Comparing {0} and {1}", $url, $mappingPair->path);
		if (pmvc_empty($mappingPair->basePath)) {
           	return pmvc_matches($mappingPair->path, $url)===TRUE;
        }
        if (substr($url, 0, strlen($mappingPair->basePath))!=$mappingPair->basePath) {
        	return FALSE;
        }
        return pmvc_matches($mappingPair->path, substr($url, strlen($mappingPair->basePath)))===TRUE; 
    }
    
    /**
     * Dispatches controller to the controller
     * that is configured for the url.
     */
    public function dispatch() {
    	
    	// invoke wrappers
    	pmvc_logdebug("Calling RequestWrappers beforeRequest()");
    	$this->dispatch_RequestWrapper(true);
    	if ($this->pendingRedirect!==FALSE) {
    		pmvc_logdebug("Sending redirect to: {0}", $this->pendingRedirect);
    		header("Location: ".$this->pendingRedirect);
    		return;
    	}
    	
    	// dispatch to controller
    	pmvc_logdebug("Calling Controller handleRequest()");
    	$mav = $this->dispatch_Controller();
    	if ($this->pendingRedirect!==FALSE) {
    		pmvc_logdebug("Sending redirect to: {0}", $this->pendingRedirect);
    		header("Location: ".$this->pendingRedirect);
    		return;
    	}
    	
    	// handle model and view
        if ($mav===FALSE || !($mav instanceof ModelAndView)) {
            pmvc_logdebug(
            	"no ModelAndView to render (or not a ModelAndView)"
				.", not invoking a ViewHandler");
				
		// dispatch to view handler
        } else {
    		pmvc_logdebug("Calling ViewHandler renderView()");
    		$this->dispatch_ViewHandler($mav);
        }
    	
    	// invoke wrappers
    	pmvc_logdebug("Calling RequestWrappers afterRequest()");
    	$this->dispatch_RequestWrapper(false);
    }
    
    private function dispatch_RequestWrapper($beforeRequest) {
    	
    	// loop through and invoke each request wrapper
        foreach ($this->requestWrapperMappings as $mappingPair) {     
            if ($this->doesUrlMatchMappingPair($this->getRequestedUrl(), $mappingPair)
            	&& $this->pendingRedirect===FALSE) {
            	
            	// found a configuration
                $objConfig = $this->wrapperConfigs[$mappingPair->toId];
                pmvc_logdebug("dispatch_RequestWrapper selected: {0}", $objConfig->id);
        		
                // instantiate it if needed
                if ($objConfig->instance===FALSE) {
		        	$objConfig->instance = pmvc_instantiate($objConfig->class);
		        	if ($objConfig->instance===FALSE 
		        		|| !($objConfig->instance instanceof RequestWrapper)) {
		        		pmvc_logfatal(
		        			"Unable to create RequestWrapper or it doesn't"
		        			." implement RequestWrapper: {0}", 
		        			$objConfig->class);
		        	}
                }
                
                // invoke it
                if ($beforeRequest) {
                	$objConfig->instance->beforeRequest();
                } else {
                	$objConfig->instance->afterRequest();
                }
                
            }
        }
    }
    
    private function dispatch_Controller($mav=FALSE) {
    	
        // loop through the controller map
        $objConfig = FALSE;
        foreach ($this->controllerMappings as $mappingPair) {
			
            // if the regex matches, use it        
            if ($this->doesUrlMatchMappingPair($this->getRequestedUrl(), $mappingPair)) {
                if (substr($mappingPair->toId, 0, 5)=="view:") {
                    return new ModelAndView(substr($mappingPair->toId, 5));
                }
                $objConfig = $this->controllerConfigs[$mappingPair->toId];
                pmvc_logdebug("dispatch_Controller() selected: {0}", $objConfig->id);
                break;
            }
        }
        
        // was a controller found? if not - do we have a 404 controller?
        if ($objConfig===FALSE && !pmvc_empty($this->fourOhFourController)) {
        	header("HTTP/1.0 404 Not Found");
        	$objConfig = $this->controllerConfigs[$this->fourOhFourController];
        	
        } else if ($objConfig===FALSE && pmvc_empty($this->fourOhFourController)) {
        	header("HTTP/1.0 404 Not Found");
        	pmvc_logfatal("Controler for url not found: {0}", $this->getRequestedUrl());
        }
        
        // do we have a controller configured for this?
        if ($objConfig!==FALSE) {
        		
        	// create it
        	$controller = pmvc_instantiate($objConfig->class);
        	if ($controller===FALSE || !($controller instanceof Controller)) {
        		pmvc_logfatal(
        			"Unable to create controller or it doesn't implement Controller: {0}", 
        			$ctrlConfig->class);
        	}
        	
        	// configure and invoke
        	$controller->configure($objConfig->config);
            $mav = $controller->handleRequest();
        	
        }
        
        // should we re-dispatch?
        if ($mav!==FALSE 
            && ($mav instanceof ModelAndView)
            && substr($mav->getViewName(), 0, strlen("forward:"))=="forward:") {
        	$this->requestedUrl = substr($mav->getViewName(), strlen("forward:"));
        	$newMav = $this->dispatch_Controller($mav);
        	
	        // merge the models
        	if ($newMav!==FALSE) {
	        	$model = $mav->getModel();
	        	foreach ($model as $key => $value) {
	        		if (!array_key_exists($key, $model)) {
	        			$model[$key] = $value;
	        		}
	        	}
	        	$mav = new ModelAndView($newMav->getViewName(), $model);
	        	
        	} else {
        		$mav = FALSE;
        	}
        }
        
        // return ModelAndView
        return $mav;
    }
    
    private function dispatch_ViewHandler($mav) {
        
       	// loop through the mav map
        $objConfig = FALSE;
        pmvc_assert(pmvc_empty($mav->getViewName())!==TRUE, "No view name specified");
        
        // find a handler for the view
        foreach ($this->viewHandlerMappings as $mappingPair) {
            if ($this->doesUrlMatchMappingPair($mav->getViewName(), $mappingPair)) {
                $objConfig = $this->viewHandlerConfigs[$mappingPair->toId];
        		pmvc_logdebug("dispatch() selected view handler: {0}", $objConfig->id);
                break;
            }
        }

        // invoke the handler
        if ($objConfig!==FALSE) {
        	$hndlr = pmvc_instantiate($objConfig->class);
			if ($hndlr===FALSE || !($hndlr instanceof ViewHandler)) {
        		pmvc_logfatal(
        			"Unable to create view handler or it doesn't implement ViewHandler: {0}", 
        			$handlerCfg->class);
        	}
        	
        	// configure and invoke
        	$hndlr->configure($objConfig->config);
            $mav = $hndlr->renderView($mav);
        	
        } else {
        	pmvc_loginfo(
        		"No ModelAndViewHandler configured for view {0}, assuming controller handled ", 
        		$mav->getViewName());
        }
        
    }
	
}

class MappingPair {
	public function MappingPair($path, $toId, $basePath=NULL) {
		$this->path 	= $path;
		$this->toId		= $toId;
		$this->basePath	= $basePath;
	}
	public $basePath	= FALSE;
	public $path		= FALSE;
	public $toId		= FALSE;
}

class ObjectConfiguration {
    public function ObjectConfiguration(
        $id=FALSE, $class=FALSE, $config=Array(), $instance=FALSE) {
        $this->id			= $id;
        $this->class		= $class;
        $this->config		= $config;
        $this->instance	    = $instance;
    }
	public $id			= FALSE;
	public $class		= FALSE;
	public $config		= Array();
	public $instance	= FALSE;
}


?>
